<?php

namespace pmvc\ioc;

use Exception;

/**
 * An {@link ObjectFactory} that uses XML
 * configuration files for configuration.
 *
 */
class XmlObjectFactory
	extends DescriptorObjectFactory {

	/**
	 * Creates the {@link XmlObjectFactory} using the
	 * given xml files.
	 * @param $xmlFiles the files to add
	 */
	public function __construct($xmlFiles=Array(), ObjectFactory $parent=NULL) {
		parent::__construct($parent);
		foreach ($xmlFiles as $xmlFile) {
			$this->addXmlFile($xmlFile);
		}
	}

	/**
	 * Adds an xml object configuration file to the
	 * {@link ObjectFactory}.
	 * @param string $xmlFile the file to add
	 */
	public function addXmlFile($xmlFile) {
		
		// load the configuration file
		$xml = simplexml_load_file($xmlFile);
		if ($xml===FALSE) {
			throw new Exception(
				"Unable to parse configuration file: ".$xmlFile);
		}

		// load properties
		foreach ($xml->xpath("/objects/*") as $objectEl) {

			// <import file="" />
			if ($objectEl->getName()=="import") {
				$this->assimilateImport($objectEl, $xmlFile);

			// <properties ...
			} else if ($objectEl->getName()=="properties") {
				$this->assimilateProperties($objectEl, $xmlFile);

			// <object ...
			} else if ($objectEl->getName()=="object") {
				$this->assimilateObject($objectEl, $xmlFile);
			}
		}
	}

	private function assimilateImport($importEl, $xmlFile) {
		$file = $this->getPath($this->attrValue($importEl, "file"), $xmlFile);
		if (!file_exists($file)) {
			throw new Exception(
				"Cant find file: ".$this->attrValue($objectEl, "file"));
		}
		$this->addXmlFile($file);
	}

	private function assimilateProperties($propertiesEl, $xmlFile) {
		$addEnvironmentVariables		= $this->attrValue($propertiesEl, "add-env-vars",		"false");
		$environmentVariablesOverride	= $this->attrValue($propertiesEl, "env-vars-override",	"false");
		
		$requiredFiles = Array();
		$optionalFiles = Array();
		
		// check file attribute
		$fileAttr = $this->attrValue($propertiesEl, 'file');
		if (isset($fileAttr)) {
			// create file descriptor from attributes
			$propDesc = new PropertyDescriptor(NULL, $this->getPath($fileAttr, $xmlFile), PropertyDescriptor::TYPE_STRING);
			if ($this->attrValue($propertiesEl, 'optional') != 'true') {
				$requiredFiles[] = $propDesc;
			} else {
				$optionalFiles[] = $propDesc;
			}
		} else {
			// create file descriptors from file sub-elements
			foreach ($propertiesEl->xpath('file') as $fileEl) {
				$propDesc = new PropertyDescriptor(NULL, $this->getPath((string)$fileEl, $xmlFile), PropertyDescriptor::TYPE_STRING);
				if ($this->attrValue($fileEl, 'optional') != 'true') {
					$requiredFiles[] = $propDesc;
				} else {
					$optionalFiles[] = $propDesc;
				}
			}
		}
		
		if (empty($requiredFiles) && empty($optionalFiles)) {
			\pmvc\log\Log::warn("No files specified for <properties> tag");
		}

		// create ObjectDescriptor for PropertiesFactory
		$propertiesId = $this->attrValue($propertiesEl, 'id');
		if (!isset($propertiesId)) {
			$propertiesId = uniqid('', TRUE) . '#Properties';
		}
		$propertiesDesc = new ObjectDescriptor(
			$propertiesId, $propertiesId, "pmvc\Properties", Array(
				new PropertyDescriptor("addEnvironmentVariables", 		$addEnvironmentVariables=="true", 		PropertyDescriptor::TYPE_BOOLEAN),
				new PropertyDescriptor("environmentVariablesOverride", 	$environmentVariablesOverride=="true", 	PropertyDescriptor::TYPE_BOOLEAN),
				new PropertyDescriptor("requiredPropertyFiles", 		$requiredFiles, 						PropertyDescriptor::TYPE_ARRAY),
				new PropertyDescriptor("optionalPropertyFiles", 		$optionalFiles, 						PropertyDescriptor::TYPE_ARRAY)
			),
			Array(), TRUE, FALSE, NULL, FALSE, NULL, NULL, "pmvc\ioc\PropertiesFactory");
		parent::addObjectDescriptor($propertiesDesc);
		
		// add to ObjectDescriptor
		$id = uniqid('', TRUE)."#PropertiesDescriptorPostProcessor";
		$desc = new ObjectDescriptor(
			$id, $id, "pmvc\ioc\PropertiesDescriptorPostProcessor", Array(
				new PropertyDescriptor("properties", $propertiesId, PropertyDescriptor::TYPE_REF)
			));
		parent::addObjectDescriptor($desc);
		return $desc;
	}

	private function assimilateObject($objectEl, $xmlFile) {
		// get constructor args
		$constructorArgs = Array();
		foreach ($objectEl->xpath("constructor-arg") as $ctrEl) {
			array_push($constructorArgs, $this->assimilateProperty($ctrEl, $xmlFile));
		}
		
		// get properties
		$properties = Array();
		foreach ($objectEl->xpath("property") as $propertyEl) {
			array_push($properties, $this->assimilateProperty($propertyEl, $xmlFile));
		}

		// get class data
		$class			= $this->attrValue($objectEl, "class");
		$factoryClass	= $this->attrValue($objectEl, "factoryClass");
		$uniqid			= uniqid('', TRUE)."#".$class;
		
		$id				= $this->attrValue($objectEl, "id", $uniqid);
		$name			= $this->attrValue($objectEl, "name");
		$singleton		= $this->attrValue($objectEl, "singleton", "true");
		$abstract		= $this->attrValue($objectEl, "abstract", "false");
		$parent			= $this->attrValue($objectEl, "parent");
		$lazy			= $this->attrValue($objectEl, "lazy-init", "true");
		$initMethod		= $this->attrValue($objectEl, "init-method");
		$destroyMethod	= $this->attrValue($objectEl, "destroy-method");
		$preloadFile	= $this->attrValue($objectEl, "preloadFile");

		// add the descriptor
		$desc = new ObjectDescriptor(
			$id, $name, $class, $properties, $constructorArgs, 
			strtolower($singleton)=="true", strtolower($abstract)=="true",
			$parent, strtolower($lazy)=="true", $initMethod, $destroyMethod,
			$factoryClass, $preloadFile);
		parent::addObjectDescriptor($desc);
		return $desc;
	}

	private function assimilateArray($arrayEl, $xmlFile) {
		$value = Array();
		foreach ($arrayEl->xpath("entry") as $entryEl) {
			array_push($value, $this->assimilateProperty($entryEl, $xmlFile));
		}
		return $value;
	}

	private function assimilateProperty($propertyEl, $xmlFile) {

		// get type name and value
		$type	= $this->attrValue($propertyEl, "type",	PropertyDescriptor::TYPE_STRING);
		$name	= $this->attrValue($propertyEl, "name", $propertyEl->name);
		$value	= $this->attrValue($propertyEl, "value", $propertyEl->value);

		// if we don't have a value try the "ref" element
		if ($value==NULL || !$value) {
			$value = $this->attrValue($propertyEl, "ref", $propertyEl->ref);
			if ($value!=NULL && $value) {
				$type = PropertyDescriptor::TYPE_REF;
			}
		}

		// if it's not a ref lets assimilate arrays and objects
		if ($type!=PropertyDescriptor::TYPE_REF) {

			// is this an array?
			if (empty($value) && $propertyEl->array) {
				$value = $this->assimilateArray($propertyEl->array, $xmlFile);
				$type = PropertyDescriptor::TYPE_ARRAY;

			// is this an object?
			} else if (empty($value) && $propertyEl->object) {
				$objDesc = $this->assimilateObject($propertyEl->object, $xmlFile);
				$value = $objDesc->id;
				$type = PropertyDescriptor::TYPE_REF;
			}
		}

		// create the property descriptor
		return new PropertyDescriptor($name, $value, $type);
	}

	private function attrValue($element, $name, $default=NULL) {
		$val = (string)$element[$name];
		return  (!empty($val))
			? (string)$element[$name] : $default;
	}

	private function getPath($file, $relative) {
		if (file_exists($file)) {
			return $file;
			
		} else if (substr($file, 0, 1)!="/"
			&& file_exists(dirname($relative)."/".$file)) {
			return dirname($relative)."/".$file;
			
		} else if (substr($file, 0, 1)=="/"
			&& file_exists(dirname($relative).$file)) {
			return dirname($relative).$file;
			
		} else {
			return $file;
		}
	}

}

?>